/*******************************************************************************
* Copyright (c) 2013-2015 Sierra Wireless and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Sierra Wireless - initial API and implementation
* Bosch Software Innovations - added Redis URL support with authentication
*******************************************************************************/
package org.eclipse.leshan.server.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.BindException;
import java.net.URI;
import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.leshan.LwM2m;
import org.eclipse.leshan.core.model.ObjectLoader;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
import org.eclipse.leshan.server.californium.LeshanServerBuilder;
import org.eclipse.leshan.server.californium.impl.LeshanServer;
import org.eclipse.leshan.server.cluster.RedisRegistrationStore;
import org.eclipse.leshan.server.cluster.RedisSecurityStore;
import org.eclipse.leshan.server.demo.servlet.ClientServlet;
import org.eclipse.leshan.server.demo.servlet.EventServlet;
import org.eclipse.leshan.server.demo.servlet.ObjectSpecServlet;
import org.eclipse.leshan.server.demo.servlet.SecurityServlet;
import org.eclipse.leshan.server.impl.FileSecurityStore;
import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.eclipse.leshan.server.model.StaticModelProvider;
import org.eclipse.leshan.server.security.EditableSecurityStore;
import org.eclipse.leshan.util.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.util.Pool;
public class LeshanServerDemo {
private static final Logger LOG = LoggerFactory.getLogger(LeshanServerDemo.class);
private final static String[] modelPaths = new String[] { "LWM2M_Lock_and_Wipe-V1_0.xml",
"LWM2M_Cellular_connectivity-v1_0.xml", "LWM2M_APN_connection_profile-v1_0.xml",
"LWM2M_WLAN_connectivity4-v1_0.xml", "LWM2M_Bearer_selection-v1_0.xml",
"LWM2M_Portfolio-v1_0.xml", "Communication_Characteristics-V1_0.xml",
"Non-Access_Stratum_NAS_configuration-V1_0.xml", "3200.xml", "3201.xml", "3202.xml",
"3203.xml", "3300.xml", "3301.xml", "3302.xml", "3303.xml", "3304.xml", "3305.xml",
"3306.xml", "3308.xml", "3310.xml", "3311.xml", "3312.xml", "3313.xml", "3314.xml",
"3315.xml", "3316.xml", "3317.xml", "3318.xml", "3319.xml", "3320.xml", "3321.xml",
"3322.xml", "3323.xml", "3324.xml", "3325.xml", "3326.xml", "3327.xml", "3328.xml",
"3329.xml", "3330.xml", "3331.xml", "3332.xml", "3333.xml", "3334.xml", "3335.xml",
"3336.xml", "3337.xml", "3338.xml", "3339.xml", "3340.xml", "3341.xml", "3342.xml",
"3343.xml", "3344.xml", "3345.xml", "3346.xml", "3347.xml", "3348.xml", "singlePhasePM.xml",
"VehicleControlUnit.xml", "Application-Data-Container.xml", "LWM2M_DevCapMgmt-v1_0.xml",
"LWM2M_Software_Component-v1_0.xml", "LWM2M_Software_Management-v1_0.xml",
"3-PhasePM.xml", "ActiveCmdhPolicy.xml", "CmdhBackOffParametersSet.xml", "CmdhBuffer.xml",
"CmdhDefaults.xml", "CmdhDefEcValues.xml", "CmdhEcDefParamValues.xml", "CmdhLimits.xml",
"CmdhNetworkAccessRules.xml", "CmdhNwAccessRule.xml", "CmdhPolicy.xml" };
private final static String USAGE = "java -jar leshan-server-demo.jar [OPTION]";
private final static String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType();
private final static String DEFAULT_KEYSTORE_ALIAS = "leshan";
public static void main(String[] args) {
// Define options for command line tools
Options options = new Options();
options.addOption("h", "help", false, "Display help information.");
options.addOption("lh", "coaphost", true, "Set the local CoAP address.\n Default: any local address.");
options.addOption("lp", "coapport", true,
String.format("Set the local CoAP port.\n Default: %d.", LwM2m.DEFAULT_COAP_PORT));
options.addOption("slh", "coapshost", true, "Set the secure local CoAP address.\nDefault: any local address.");
options.addOption("slp", "coapsport", true,
String.format("Set the secure local CoAP port.\nDefault: %d.", LwM2m.DEFAULT_COAP_SECURE_PORT));
options.addOption("ks", "keystore", true,
"Set the key store file. If set, X.509 mode is enabled, otherwise built-in RPK credentials are used.");
options.addOption("ksp", "storepass", true, "Set the key store password.");
options.addOption("kst", "storetype", true,
String.format("Set the key store type.\nDefault: %s.", DEFAULT_KEYSTORE_TYPE));
options.addOption("ksa", "alias", true, String.format(
"Set the key store alias to use for server credentials.\nDefault: %s.", DEFAULT_KEYSTORE_ALIAS));
options.addOption("ksap", "keypass", true, "Set the key store alias password to use.");
options.addOption("wp", "webport", true, "Set the HTTP port for web server.\nDefault: 8080.");
options.addOption("m", "modelsfolder", true, "A folder which contains object models in OMA DDF(.xml) format.");
options.addOption("r", "redis", true,
"Set the location of the Redis database for running in cluster mode. The URL is in the format of: 'redis://:password@hostname:port/db_number'\nExample without DB and password: 'redis://localhost:6379'\nDefault: none, no Redis connection.");
HelpFormatter formatter = new HelpFormatter();
formatter.setOptionComparator(null);
// Parse arguments
CommandLine cl;
try {
cl = new DefaultParser().parse(options, args);
} catch (ParseException e) {
System.err.println("Parsing failed. Reason: " + e.getMessage());
formatter.printHelp(USAGE, options);
return;
}
// Print help
if (cl.hasOption("help")) {
formatter.printHelp(USAGE, options);
return;
}
// Abort if unexpected options
if (cl.getArgs().length > 0) {
System.err.println("Unexpected option or arguments : " + cl.getArgList());
formatter.printHelp(USAGE, options);
return;
}
// get local address
String localAddress = cl.getOptionValue("lh");
String localPortOption = cl.getOptionValue("lp");
int localPort = LwM2m.DEFAULT_COAP_PORT;
if (localPortOption != null) {
localPort = Integer.parseInt(localPortOption);
}
// get secure local address
String secureLocalAddress = cl.getOptionValue("slh");
String secureLocalPortOption = cl.getOptionValue("slp");
int secureLocalPort = LwM2m.DEFAULT_COAP_SECURE_PORT;
if (secureLocalPortOption != null) {
secureLocalPort = Integer.parseInt(secureLocalPortOption);
}
// get http port
String webPortOption = cl.getOptionValue("wp");
int webPort = 8080;
if (webPortOption != null) {
webPort = Integer.parseInt(webPortOption);
}
// Get models folder
String modelsFolderPath = cl.getOptionValue("m");
// get the Redis hostname:port
String redisUrl = cl.getOptionValue("r");
// Get keystore parameters
String keyStorePath = cl.getOptionValue("ks");
String keyStoreType = cl.getOptionValue("kst", KeyStore.getDefaultType());
String keyStorePass = cl.getOptionValue("ksp");
String keyStoreAlias = cl.getOptionValue("ksa");
String keyStoreAliasPass = cl.getOptionValue("ksap");
try {
createAndStartServer(webPort, localAddress, localPort, secureLocalAddress, secureLocalPort,
modelsFolderPath, redisUrl, keyStorePath, keyStoreType, keyStorePass, keyStoreAlias,
keyStoreAliasPass);
} catch (BindException e) {
System.err.println(
String.format("Web port %s is already used, you could change it using 'webport' option.", webPort));
formatter.printHelp(USAGE, options);
} catch (Exception e) {
LOG.error("Jetty stopped with unexpected error ...", e);
}
}
public static void createAndStartServer(int webPort, String localAddress, int localPort, String secureLocalAddress,
int secureLocalPort, String modelsFolderPath, String redisUrl, String keyStorePath, String keyStoreType,
String keyStorePass, String keyStoreAlias, String keyStoreAliasPass)
throws Exception {
// Prepare LWM2M server
LeshanServerBuilder builder = new LeshanServerBuilder();
builder.setLocalAddress(localAddress, localPort);
builder.setLocalSecureAddress(secureLocalAddress, secureLocalPort);
builder.setEncoder(new DefaultLwM2mNodeEncoder());
LwM2mNodeDecoder decoder = new DefaultLwM2mNodeDecoder();
builder.setDecoder(decoder);
builder.setNetworkConfig(NetworkConfig.getStandard());
// connect to redis if needed
Pool<Jedis> jedis = null;
if (redisUrl != null) {
// TODO: support sentinel pool and make pool configurable
jedis = new JedisPool(new URI(redisUrl));
}
PublicKey publicKey = null;
// Set up X.509 mode
if (keyStorePath != null) {
try {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
try (FileInputStream fis = new FileInputStream(keyStorePath)) {
keyStore.load(fis, keyStorePass == null ? null : keyStorePass.toCharArray());
List<Certificate> trustedCertificates = new ArrayList<>();
for (Enumeration<String> aliases = keyStore.aliases(); aliases.hasMoreElements();) {
String alias = aliases.nextElement();
if (keyStore.isCertificateEntry(alias)) {
trustedCertificates.add(keyStore.getCertificate(alias));
} else if (keyStore.isKeyEntry(alias) && alias.equals(keyStoreAlias)) {
List<X509Certificate> x509CertificateChain = new ArrayList<>();
Certificate[] certificateChain = keyStore.getCertificateChain(alias);
if (certificateChain == null || certificateChain.length == 0) {
LOG.error("Keystore alias must have a non-empty chain of X509Certificates.");
System.exit(-1);
}
for (Certificate certificate : certificateChain) {
if (!(certificate instanceof X509Certificate)) {
LOG.error("Non-X.509 certificate in alias chain is not supported: {}", certificate);
System.exit(-1);
}
x509CertificateChain.add((X509Certificate) certificate);
}
Key key = keyStore.getKey(alias,
keyStoreAliasPass == null ? new char[0] : keyStoreAliasPass.toCharArray());
if (!(key instanceof PrivateKey)) {
LOG.error("Keystore alias must have a PrivateKey entry, was {}",
key == null ? null : key.getClass().getName());
System.exit(-1);
}
builder.setPrivateKey((PrivateKey) key);
publicKey = keyStore.getCertificate(alias).getPublicKey();
builder.setCertificateChain(
x509CertificateChain.toArray(new X509Certificate[x509CertificateChain.size()]));
}
}
builder.setTrustedCertificates(
trustedCertificates.toArray(new Certificate[trustedCertificates.size()]));
}
} catch (KeyStoreException | IOException e) {
LOG.error("Unable to initialize X.509.", e);
System.exit(-1);
}
}
// Otherwise, set up RPK mode
else {
try {
// Get point values
byte[] publicX = Hex
.decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray());
byte[] publicY = Hex
.decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray());
byte[] privateS = Hex
.decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray());
// Get Elliptic Curve Parameter spec for secp256r1
AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC");
algoParameters.init(new ECGenParameterSpec("secp256r1"));
ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class);
// Create key specs
KeySpec publicKeySpec = new ECPublicKeySpec(
new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), parameterSpec);
KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec);
// Get keys
publicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec);
PrivateKey privateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec);
builder.setPublicKey(publicKey);
builder.setPrivateKey(privateKey);
} catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidParameterSpecException e) {
LOG.error("Unable to initialize RPK.", e);
System.exit(-1);
}
}
// Define model provider
List<ObjectModel> models = ObjectLoader.loadDefault();
models.addAll(ObjectLoader.loadDdfResources("/models/", modelPaths));
if (modelsFolderPath != null) {
models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath)));
}
LwM2mModelProvider modelProvider = new StaticModelProvider(models);
builder.setObjectModelProvider(modelProvider);
// Set securityStore & registrationStore
EditableSecurityStore securityStore;
if (jedis == null) {
// use file persistence
securityStore = new FileSecurityStore();
} else {
// use Redis Store
securityStore = new RedisSecurityStore(jedis);
builder.setRegistrationStore(new RedisRegistrationStore(jedis));
}
builder.setSecurityStore(securityStore);
// Create and start LWM2M server
LeshanServer lwServer = builder.build();
// Now prepare Jetty
Server server = new Server(webPort);
WebAppContext root = new WebAppContext();
root.setContextPath("/");
root.setResourceBase(LeshanServerDemo.class.getClassLoader().getResource("webapp").toExternalForm());
root.setParentLoaderPriority(true);
server.setHandler(root);
// Create Servlet
EventServlet eventServlet = new EventServlet(lwServer, lwServer.getSecureAddress().getPort());
ServletHolder eventServletHolder = new ServletHolder(eventServlet);
root.addServlet(eventServletHolder, "/event/*");
ServletHolder clientServletHolder = new ServletHolder(
new ClientServlet(lwServer, lwServer.getSecureAddress().getPort()));
root.addServlet(clientServletHolder, "/api/clients/*");
ServletHolder securityServletHolder = new ServletHolder(new SecurityServlet(securityStore, publicKey));
root.addServlet(securityServletHolder, "/api/security/*");
ServletHolder objectSpecServletHolder = new ServletHolder(new ObjectSpecServlet(lwServer.getModelProvider()));
root.addServlet(objectSpecServletHolder, "/api/objectspecs/*");
// Start Jetty & Leshan
lwServer.start();
server.start();
LOG.info("Web server started at {}.", server.getURI());
}
}